﻿using System;
using System.Data.SqlClient;
using System.Linq;
using Microsoft.Xrm.Sdk;
using System.Transactions;

namespace Crm2011.BusinessTransaction
{
    /// <summary>
    /// Crm Plugin which initiates business transaction
    /// </summary>
    public class BusinessTransactionPlugin : IPlugin
    {

        public void Execute(IServiceProvider serviceProvider)
        {
            
            try
            {
                var executionContext = new ExecutionContext();
                // Obtain the execution context from the service provider.
                executionContext.Context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

                //Extract the tracing service for use in debugging sandboxed plug-ins.
                executionContext.TracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));


                // Obtain the target entity from the input parmameters.
                if (executionContext.Context.InputParameters.Contains("Target") && executionContext.Context.InputParameters["Target"] is Entity)
                    executionContext.TargetEntity = (Entity)executionContext.Context.InputParameters["Target"];

                if (executionContext.TargetEntity == null)
                    throw new ArgumentException("TargetEntity");
                
                //make sure the plugin is attached to correct entity
                if (executionContext.TargetEntity.LogicalName.ToLower() != "new_businesstransaction")
                    throw new Exception(
                        "Plugin must be attached to new_businesstransaction entity. Currently attached to " +
                        executionContext.TargetEntity.LogicalName);

                // Obtain the organization service reference.
                var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                executionContext.Service = serviceFactory.CreateOrganizationService(executionContext.Context.UserId);

                executionContext.TracingService.Trace(this.GetType().Name + " plugin: Executing custom code...");

                this.ExecuteBusinessTransaction(executionContext);
            }
            catch (Exception ex)
            {
                throw new InvalidPluginExecutionException(this.GetType().Name + "plugin: An error occurred.", ex);
                throw;
            }
            
        }
        
        private void ExecuteBusinessTransaction(ExecutionContext executionContext)
        {
            BaseProcess process = null;
            string processTypeName = "";
            string processName = "";
            try
            {
                //Retrieve Business process name
                processName = executionContext.TargetEntity.Attributes["new_businessprocessname"].ToString();
                
                //Retrieve any data associated with business process
                //Serialized XML can be an example of conmplex data passed into Business process
                object processData = null;
                if (executionContext.TargetEntity.Attributes.Contains("new_data"))
                    processData = executionContext.TargetEntity.Attributes["new_data"];

                //instantiate business process
                processTypeName = typeof(BaseProcess).Namespace + "." + processName;
                Type type = typeof(BaseProcess).Assembly.GetType(processTypeName);                
                if (type == null)
                    throw new Exception("Cannot create type " + processTypeName);

                process = Activator.CreateInstance(type, new object[] { executionContext, processData }) as BaseProcess;
                
                if (process == null)
                    throw new Exception("Unable to instantiate an object of type '" + processTypeName + "'");
                else
                {

                    //extract resal SQL transaction and connection from Plugin execution context
                    object crmTransaction = null;
                    SqlTransaction pluginTransaction = null;
                    System.Data.IsolationLevel originalIsolationLevel =  System.Data.IsolationLevel.Unspecified;

                    try
                    {
                        var platformContext = executionContext.Context.GetType().InvokeMember("PlatformContext", System.Reflection.BindingFlags.GetProperty, null, executionContext.Context, null);
                        if (platformContext != null)
                        {
                            crmTransaction = platformContext.GetType().BaseType.GetField("_transaction", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
                                .GetValue(platformContext);
                            pluginTransaction = (SqlTransaction)crmTransaction.GetType().GetField("transaction", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
                                .GetValue(crmTransaction);
                        }
                    }
                    catch (Exception)
                    {
                    }

                    //if transaction is found then commit it but preserve its connection first.
                    //Committing transaction will dispose it and set connection property = null                    
                    if (pluginTransaction != null)
                    {
                        originalIsolationLevel = pluginTransaction.IsolationLevel;
                        executionContext.PluginConnection = pluginTransaction.Connection;
                        pluginTransaction.Commit();
                        executionContext.TracingService.Trace("Business transaction CRM record committed!");
                    }

                    //initiate new enterprise transaction scope
                    using (TransactionScope txScope = new TransactionScope(TransactionScopeOption.Required,
                                                       new TransactionOptions()
                                                       {
                                                           IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted,
                                                           Timeout = new TimeSpan (0,10,0)
                                                       },
                                                       EnterpriseServicesInteropOption.Full))
                    {
                        executionContext.TracingService.Trace("Enterprise transaction BEGIN");

                        //enlist plugin connection in transaction scope so changes made by this connection 
                        //become visible to other connections opened within the scope
                        if (executionContext.PluginConnection != null)
                        {
                            executionContext.PluginConnection.EnlistTransaction(System.Transactions.Transaction.Current);
                            executionContext.TracingService.Trace("Plugin connection enlisted in Enterprise transaction");
                        }
                        try
                        {
                            //Execute our custom logic
                            executionContext.TracingService.Trace("Executing business process: " + processName);
                            process.Execute();
                            executionContext.TracingService.Trace("Finished business process: " + processName);

                            //After all we update previously committed row in CRM with Completed flag
                            //this change is also part of big transaction
                            executionContext.TargetEntity.Attributes["new_completed"] = true;
                            executionContext.Service.Update(executionContext.TargetEntity);

                            //Complete transaction
                            txScope.Complete();
                            executionContext.TracingService.Trace("Enterprise transaction COMPLETED");

                        }
                        catch (Exception)
                        {
                            executionContext.TracingService.Trace("Enterprise transaction ROLLBACK");
                            throw;
                        }
                    }

                    //Restoring original image in crm plugin internals
                    if (executionContext.PluginConnection != null)
                    {
                        executionContext.TracingService.Trace("Delegating transaction control back to plugin pipline");
                        //un-enlist transaction 
                        executionContext.PluginConnection.EnlistTransaction(null);
                        //begin new transaction similar to one been committed before
                        var newPluginTransaction = executionContext.PluginConnection.BeginTransaction(originalIsolationLevel);
                        //store this transaction inside CrmTransaction object
                        crmTransaction.GetType().GetField("transaction", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
                            .SetValue(crmTransaction, newPluginTransaction);

                    }
                }

            }
            catch (Exception ex)
            {
                if (process == null)
                    throw new Exception("Unable to instantiate an object of type '" + processTypeName + "'", ex);
                else
                    throw new Exception("Execution failed: " + processName, ex);
            }

        }

    }


}